local op_lib = require("lib.rule_core.operator")
local var_lib = require("lib.rule_core.var")
local value_lib = require("lib.rule_core.value")
local act_lib = require("lib.rule_core.act")
local config = require("config")
local util = require("lib.util")
local cjson_safe = require("cjson.safe")
local moses = require("lib.packages.moses_min")
local string_util = require("lib.string_util")
local ipmatcher

cjson_safe.encode_escape_forward_slash(false)
cjson_safe.encode_empty_table_as_object(false)

if not g_ipmatcher then
    if (require("ffi")).os == "Windows" then
        ipmatcher = require("lib.packages.ipmatcher_win")
    else
        ipmatcher = require("lib.packages.ipmatcher")
    end
else
    ipmatcher = g_ipmatcher
end

local _M = {}

---读取规则文件内容
---@param fh file* 要读取的文件句柄
---@return string
local function read_rule_file_content(fh)
    local result, line_str = "", ""
    for line in fh:lines("*l") do
        line_str = ""
        -- 以 ## 开头的行是注释,不处理; 空行,不处理;
        local from = ngx.re.find(line, "(^\\s*##)|(^\\s*$)", "jo")
        if not from then
            line_str = string_util.trim(line)

            if line_str ~= "" then
                -- 将非 \" 的 \ 都变成 \\
                local newstr = ngx.re.gsub(line_str, "(\\\\(?!\"))", "\\\\", "jo")
                if newstr then
                    -- 将 \" 变成 \\\"
                    newstr = ngx.re.gsub(newstr, "(\\\\\")", "\\\\\\\"", "jo")
                    if newstr then
                        line_str = newstr
                    end
                end
                result = result .. line_str
            end
        end
    end

    return result
end

local load_all_normal_rules
do
    local result = {}

    ---从文件加载规则到result数组中
    ---@param rule_file string 规则文件路径
    local function _load_normal_rule(rule_file)
        local json_str = ""
        local fh, io_err = io.open(rule_file, "r")
        if fh == nil then
            ngx.log(ngx.ERR, "[easy_waf] rule file open FAIL(" .. rule_file .. "), reason:" .. tostring(io_err))
        else
            json_str = read_rule_file_content(fh)
            fh:close()

            local res, err = cjson_safe.decode(json_str)
            if res ~= nil and type(res) == "table" then
                for _, rule_item in ipairs(res) do
                    local tmp_v = {}
                    for key, value in pairs(rule_item) do
                        tmp_v[string.lower(key)] = value
                    end

                    -- 所有var op act 的值全部转为小写
                    if type(tmp_v["var"]) == "table" then
                        local tmp_var_tbl = {}
                        for _, var_item in ipairs(tmp_v["var"]) do
                            table.insert(tmp_var_tbl, string.lower(var_item))
                        end
                        tmp_v["var"] = tmp_var_tbl
                    elseif type(tmp_v["var"]) == "string" then
                        tmp_v["var"] = string.lower(tmp_v["var"])
                    end
                    if type(tmp_v["op"]) == "string" then
                        tmp_v["op"] = string.lower(tmp_v["op"])
                    end
                    if type(tmp_v["act"]) == "string" then
                        tmp_v["act"] = string.lower(tmp_v["act"])
                    end

                    -- var 合法性验证
                    local var_res, var_err = var_lib.is_valid(tmp_v["var"])
                    -- op 合法性验证
                    local op_res, op_err = op_lib.is_valid(tmp_v["op"])
                    -- value 合法性验证
                    local value_res, value_err = value_lib.is_valid(tmp_v["value"])
                    -- act 合法性验证
                    local act_res, act_err = act_lib.is_valid(tmp_v["act"])

                    local validate_res = true
                    local err_info = "[easy_waf]"
                    if not var_res then
                        validate_res = false
                        err_info = err_info .. " [" .. (var_err or "") .. "]"
                    end
                    if not op_res then
                        validate_res = false
                        err_info = err_info .. " [" .. (op_err or "") .. "]"
                    end
                    if not value_res then
                        validate_res = false
                        err_info = err_info .. " [" .. (value_err or "") .. "]"
                    end
                    if not act_res then
                        validate_res = false
                        err_info = err_info .. " [" .. (act_err or "") .. "]"
                    end

                    if not validate_res then
                        -- 规则不合法

                        local value_str
                        if type(tmp_v["value"]) == "table" then
                            value_str = cjson_safe.encode(tmp_v["value"])
                        else
                            value_str = "\"" .. tostring(tmp_v["value"]) .. "\""
                        end
                        tmp_v["value"] = nil
                        local rule_str = cjson_safe.encode(tmp_v)
                        rule_str = string.sub(tostring(rule_str), 1, -2) .. ",\"value\":" .. value_str .. "}"
                        ngx.log(ngx.CRIT, err_info .. " @file(" .. rule_file .. "). ERR-RULE: " .. rule_str)
                    else
                        -- 规则合法
                        table.insert(result, tmp_v)
                    end
                end
            elseif err then
                -- json decode 出错
                ngx.log(ngx.CRIT,
                        "[easy_waf] load_rule @file(" ..
                        rule_file .. ") cjson.decode err: " .. (err or "") .. "\n--->>>" .. json_str .. "<<<---")
            end
        end
    end

    ---加载所有常规规则
    ---@return table
    load_all_normal_rules = function()
        -- 加载规则主目录下的常规规则
        local main_rule_files =
            util.find_file_in_dir(config.waf_base_path .. "/rules", false, ".rule")
        for _, rule_file in ipairs(main_rule_files) do
            _load_normal_rule(rule_file)
        end

        -- 加载自定义规则目录下的常规规则
        local custom_rule_files =
            util.find_file_in_dir(config.waf_base_path .. "/rules_custom", false, ".rule")
        for _, rule_file in ipairs(custom_rule_files) do
            _load_normal_rule(rule_file)
        end

        return result
    end
end

local load_all_simple_rules
do
    local result = {}
    -- 简单规则 文件名 -> 规则变量 对应关系
    local file_var = {
        args = {
            var = "args_get",
            op = "re",
            act = "deny",
            log = "on"
        },
        post = {
            var = "args_post",
            op = "re",
            act = "deny",
            log = "on"
        },
        ip_white = {
            var = "ip",
            op = "ip_match",
            act = "allow",
            log = config.ip_white_log
        },
        ip_black = {
            var = "ip",
            op = "ip_match",
            act = "deny",
            log = config.ip_black_log
        },
        cookie = {
            var = "request_cookies",
            op = "re",
            act = "deny",
            log = "on"
        },
        user_agent = {
            var = "user_agent",
            op = "re",
            act = "deny",
            log = "on"
        },
        user_agent_white = {
            var = "user_agent",
            op = "re",
            act = "allow",
            log = config.user_agent_white_log
        },
        url = {
            var = "request_uri",
            op = "re",
            act = "deny",
            log = "on"
        },
        url_white = {
            var = "request_uri",
            op = "re",
            act = "allow",
            log = config.url_white_log
        },
    }
    local file_names = {}
    for k, _ in pairs(file_var) do
        table.insert(file_names, k)
    end
    ---从文件加载简单规则到result中
    ---@param fh file* 简单规则文件句柄
    ---@param var_name string   当前简单规则名(也就是简单规则文件名)
    ---@param is_custome boolean|nil   是否为自定义简单规则(默认nil,主要区分规则名称)
    local function _load_simple_rule(fh, var_name, is_custome)
        local line_res = ""
        local name_prefix
        if not is_custome then
            name_prefix = "simp:"
        else
            name_prefix = "simp-c:"
        end
        for line in fh:lines("*l") do
            line_res = ""
            -- 以 ## 开头的行是注释,不按规则处理
            -- 空行,不处理
            local from = ngx.re.find(line, "(^\\s*##)|(^\\s*$)", "jo")
            if not from then
                -- 去首尾空白字符(含\r\n)
                line_res = string_util.trim(line)

                if line_res ~= "" then
                    -- 将非 \" 的 \ 都变成 \\
                    -- local newstr = ngx.re.gsub(line_res, "(\\\\(?!\"))", "\\\\", "jo")

                    -- 将所有 \ 都变成 \\
                    local newstr = ngx.re.gsub(line_res, "\\", "\\\\", "jo")
                    if newstr then
                        line_res = newstr
                    end
                    local item = {
                        name = name_prefix .. var_name,
                        var = file_var[var_name]["var"],
                        op = file_var[var_name]["op"] or "re",
                        value = line_res,
                        act = file_var[var_name]["act"],
                        log = file_var[var_name]["log"],
                        msg = nil,
                    }
                    item["msg"] = item["name"] .. " " .. item["op"] .. " rule."
                    table.insert(result, item)
                end
            end
        end
    end

    for _, file_name in ipairs(file_names) do
        -- 加载规则主目录下的简单规则
        local fh1 = io.open(config.waf_base_path .. "/rules/simple/" .. file_name, "r")
        if fh1 == nil then
            ngx.log(ngx.ERR, "[easy_waf] load_simple_rule:\"" .. file_name .. "\" file open FAIL.")
        else
            _load_simple_rule(fh1, file_name)
        end

        -- 加载自定义规则目录下的简单规则
        local fh2 = io.open(config.waf_base_path .. "/rules_custom/simple/" .. file_name, "r")
        if fh2 == nil then
            ngx.log(ngx.ERR, "[easy_waf] load_custom_simple_rule:\"" .. file_name .. "\" file open FAIL.")
        else
            _load_simple_rule(fh2, file_name, true)
        end
    end

    ---加载所有简单规则,获取规则table
    ---@return table
    load_all_simple_rules = function()
        local final_result = {}

        -- 合并 IP白名单 / IP黑名单
        local ip_white_values = {}
        local ip_black_values = {}
        for _, rule in ipairs(result) do
            if rule["var"] == "ip" and rule["act"] == "allow" then
                table.insert(ip_white_values, rule["value"])
            elseif rule["var"] == "ip" and rule["act"] == "deny" then
                table.insert(ip_black_values, rule["value"])
            else
                table.insert(final_result, moses.clone(rule))
            end
        end
        if #ip_white_values > 0 then
            table.insert(final_result, {
                name = "simp:ip_white",
                var = "ip",
                op = file_var["ip_white"]["op"],
                value = moses.unique(ip_white_values),
                act = file_var["ip_white"]["act"],
                log = "on",
                msg = "simp:ip_white rule.",
            })
        end

        if #ip_black_values > 0 then
            table.insert(final_result, {
                name = "simp:ip_black",
                var = "ip",
                op = file_var["ip_black"]["op"],
                value = moses.unique(ip_black_values),
                act = file_var["ip_black"]["act"],
                log = "on",
                msg = "simp:ip_black rule.",
            })
        end
        return final_result
    end
end

local _load_spider_data
do
    local result = {}

    ---加载蜘蛛数据文件
    ---@param data_file string 蜘蛛数据文件路径
    local _load_spider_file_data = function(data_file)
        local json_str = ""
        local fh = io.open(data_file, "r")
        if fh == nil then
            ngx.log(ngx.ERR, "[easy_waf] load_spider(" .. data_file .. ") file open FAIL.")
        else
            json_str = read_rule_file_content(fh)
            fh:close()

            local res, err = cjson_safe.decode(json_str)
            if res ~= nil and type(res) == "table" then
                -- disable = false|nil 添加到规则数据中,否则不加
                -- 设置有蜘蛛IP池添加到规则数据中,否则不加
                local tmp_v
                for _, v in ipairs(res) do
                    if
                        (not v["disable"]) and
                        v["ip"] and #v["ip"] > 0
                    then
                        tmp_v = {}
                        for key, value in pairs(v) do
                            tmp_v[string.lower(key)] = value
                        end
                        table.insert(result, tmp_v)
                    end
                end
            elseif err then
                ngx.log(ngx.ERR,
                        "[easy_waf][spider-cjson.decode err]\"" ..
                        data_file .. "\" " .. err .. "\n--->>>" .. json_str .. "<<<---")
            end
        end
    end

    ---加载所有蜘蛛数据
    ---@return table
    _load_spider_data = function()
        result = {}
        local spider_files = util.find_file_in_dir(config.waf_base_path .. "/rules/spider", false, ".data")
        for _, data_file in ipairs(spider_files) do
            _load_spider_file_data(data_file)
        end
        local spider_files = util.find_file_in_dir(config.waf_base_path .. "/rules_custom/spider", false, ".data")
        for _, data_file in ipairs(spider_files) do
            _load_spider_file_data(data_file)
        end

        return result
    end
    _M.load_spider_data = _load_spider_data
end

---检测两规则是否一致。
---     var + op + value 完全相同则两规则一致。
---     两规则一致返回true，否则返回false。
---     出错返回 (nil, 出错信息)
---@param rule1 table 规则1
---@param rule2 table 规则2
---@return boolean | nil
---@return nil | string
local function check_same(rule1, rule2)
    if type(rule1) ~= "table" or type(rule2) ~= "table" then
        return nil, "param expect RULE object."
    end
    if not rule1["var"] or not rule1["op"] or not rule1["value"]
        or not rule2["var"] or not rule2["op"] or not rule2["value"] then
        return nil, "param need 'var' 'op' 'value' key-value."
    end
    if type(rule1["value"]) ~= type(rule2["value"]) then
        return false
    end
    if string.lower(rule1["var"]) == string.lower(rule2["var"])
        and string.lower(rule1["op"]) == string.lower(rule2["op"])
    then
        if type(rule1["value"]) == "table" then
            -- value 是 table
            return moses.isEqual(rule1, rule2)
        else
            return rule1["value"] == rule2["value"]
        end
    end
    return false
end

local load_all_rules
do
    ---处理规则变量var
    ---@param rule_var string 单个规则变量字符串
    ---@param rule_tbl table 完整的规则数组
    ---@return table
    local process_rule_var = function(rule_var, rule_tbl)
        local var_main, son_value = var_lib.var_son_split(rule_var)

        local tmp_tbl = moses.clone(rule_tbl)


        tmp_tbl["var"] = util.kebabcase_to_underscorecase(tostring(var_main))
        if type(son_value) == "string"
            and (string.len(son_value) > 0)
        then
            tmp_tbl["var_s"] = son_value
        end

        -- 如果 规则变量 var 是
        --      "header:user-agent"
        --      "request_headers:user-agent"
        --    直接放入 规则变量 var:user_agent

        -- 如果 规则变量 var 是
        --      "header:cookie"
        --      "request_headers:cookie"
        --    直接放入 规则变量 var:cookie
        if (
                (var_main == "header" or var_main == "request_headers")
                and type(son_value) == "string"
                and string.len(son_value) > 0
            )
        then
            if util.kebabcase_to_underscorecase(son_value) == "user_agent" then
                tmp_tbl["var"] = "user_agent"
                tmp_tbl["var_s"] = nil
            elseif son_value == "cookie"
                or son_value == "cookies"
            then
                tmp_tbl["var"] = "cookie"
                tmp_tbl["var_s"] = nil
            end
        end

        return tmp_tbl
    end

    ---获取所有规则(常规规则+简单规则,验证var/op/value/act,去重后的)
    ---@param sorted_by_var_and_act boolean|nil 是否按var及act分类(默认nil) true:返回整理后的规则
    ---@return table
    load_all_rules = function(sorted_by_var_and_act)
        local result = {}
        local normal_rules = load_all_normal_rules()
        local simple_rules = load_all_simple_rules()

        -- 不加载 act:next 且 log:off 的规则(没有实际意义)
        -- 常规规则中var可以是数组,先分解开
        local res_normal = {}
        for _, rule in ipairs(normal_rules) do
            if (rule["act"] == "next") and (not util.check_switch_is_on(rule["log"])) then
                goto continue
            end

            if type(rule["var"]) == "table" then
                for _, var_item in ipairs(rule["var"]) do
                    local tmp_rule = process_rule_var(var_item, rule)
                    table.insert(res_normal, tmp_rule)
                end
            else
                local tmp_rule = process_rule_var(rule["var"], rule)
                table.insert(res_normal, tmp_rule)
            end

            ::continue::
        end

        -- 针对所有var:ip 的 value 检测，只能是 array
        for _, rule in ipairs(res_normal) do
            if rule["var"] == "ip" and type(rule["value"]) ~= "table" then
                rule["value"] = { tostring(rule["value"]) }
            end
        end

        local final_normal = {}
        -- 检查过滤相同的规则(var + op + value 相同的)
        local is_same, can_use
        local max_index = #res_normal
        for i = 1, max_index - 1, 1 do
            can_use = true
            for j = i + 1, max_index, 1 do
                is_same = check_same(res_normal[i], res_normal[j])
                if is_same then
                    -- 相同规则
                    can_use = false
                    break
                end
            end
            if can_use then
                table.insert(final_normal, res_normal[i])
            end
        end

        table.insert(final_normal, res_normal[max_index])

        for _, value in ipairs(final_normal) do
            table.insert(result, moses.clone(value))
        end

        for _, value in ipairs(simple_rules) do
            table.insert(result, moses.clone(value))
        end

        if not sorted_by_var_and_act then
            -- 不需要整理,直接返回
            return result
        end

        -- 需要按 var_key 以及 act(deny,next,allow) 整理 ,以便在各阶段使用
        local all_var_keys = var_lib.get_all_var_keys()

        local final_result = {}
        for _, v in pairs(all_var_keys) do
            final_result[v] = {}
        end

        for _, v in ipairs(result) do
            if (not v["var"])
                or (not v["act"])
                or (not all_var_keys[v["var"]])
                or (not final_result[all_var_keys[v["var"]]])
            then
                ngx.log(ngx.CRIT, "[easy_waf] finally rule structure error:" .. cjson_safe.encode(v))
            else
                if not final_result[all_var_keys[v["var"]]][v["act"]] then
                    final_result[all_var_keys[v["var"]]][v["act"]] = {}
                end
                table.insert(final_result[all_var_keys[v["var"]]][v["act"]], moses.clone(v))
            end
        end

        -- 最终规则结构
        -- {
        --     ip = {
        --         deny = {
        --             -- 一条规则
        --             {
        --                 name = "xxxx",
        --                 var = "ip",
        --                 -- 规则文件中 规则变量可用":"将变量的"具体变量名"带上, var_s 保存 "具体变量名"
        --                 --    如: 规则文件中 规则变量写成: "header:user-agent", var_s 保存 "user-agent"
        --                 --    如果没有, var_s 可以不存在
        --                 var_s = "xxx",
        --                 op = "ip_match",
        --                 value = {
        --                     "127.0.0.1/24", "127.1.1.1"
        --                 },
        --                 act = "deny",
        --                 log = "off",
        --                 msg = "本机放行."
        --             },
        --             -- { 一条规则 }
        --         },
        --         allow = {
        --             -- { 一条规则 } ,  { 一条规则 }
        --         },
        --         next = {
        --             -- { 一条规则 } ,  { 一条规则 }
        --         }
        --     },
        --     url = {
        --         --  ...
        --     }, ...
        -- }

        return final_result
    end
    _M.load_all_rules = load_all_rules
end


---蜘蛛规则数据
local spider_data
if not g_spider_data then
    spider_data = _M.load_spider_data()
else
    spider_data = g_spider_data
end

---检测来访UA+IP是否为蜘蛛规则数据中设置的。
---     - UA + IP 同时满足才是真蜘蛛,否则不是
---     - 如果验证为蜘蛛,返回 true,蜘蛛name(规则数据中的)
---     - 如果验证为非蜘蛛,返回 false
---@param ua string 来访user-agent
---@param client_ip string 来访IP
---@return boolean
---@return nil | string
_M.check_is_seo = function(ua, client_ip)
    for _, rule in ipairs(spider_data) do
        local from, to, err = ngx.re.find(ua, rule["ua"], "ijo")
        if from and rule["ip"] then
            local cache = ngx.shared.easy_waf_cache
            local seo_ip_cache_key = "is_seo:" .. client_ip
            -- 过期时间(秒)  86400:24小时  3600:1小时  7200:2小时  21600:6小时
            local cache_exp = 21600
            if not cache then
                ngx.log(ngx.ALERT, "[easy_waf] no ngx.shared.easy_waf_cache")
                local matcher = ipmatcher.new(rule["ip"])
                if matcher then
                    local ok, _ = matcher:match(client_ip)
                    if ok then
                        return true, rule["name"] or "unknown"
                    end
                else
                    ngx.log(ngx.ALERT, "[easy_waf] matcher new() FAILED.")
                end
            else
                local cache_val = cache:get(seo_ip_cache_key)
                if not cache_val then
                    -- 没缓存
                    local matcher = ipmatcher.new(rule["ip"])
                    if matcher then
                        local ok, _ = matcher:match(client_ip)
                        if ok then
                            -- 是蜘蛛,刷新缓存过期时间
                            cache:set(seo_ip_cache_key, 1, cache_exp)
                            return true, rule["name"] or "unknown"
                        else
                            -- todo IP不在蜘蛛IP池中，是否记录SEO冒充信息
                            cache:set(seo_ip_cache_key, 0, cache_exp)
                            return false
                        end
                    else
                        ngx.log(ngx.ALERT, "[easy_waf] matcher new() FAILED.")
                    end
                elseif cache_val == 1 then
                    -- 是蜘蛛,刷新缓存过期时间
                    cache:set(seo_ip_cache_key, 1, cache_exp)
                    return true, rule["name"] or "unknown"
                else
                    -- todo IP不在蜘蛛IP池中，是否记录SEO冒充信息
                    cache:set(seo_ip_cache_key, 0, cache_exp)
                    return false
                end
            end
            return false
        end
    end
    return false
end

---操作执行，返回 true/false。
---     出错返回 nil, 错误信息
---@param value_var string 变量1(规则变量的值)
---@param operator string 操作符
---@param value_rule any 变量2(规则值的值)
---@return boolean | nil
---@return string | nil
_M.execute = function(value_var, operator, value_rule)
    return op_lib.execute(operator, value_var, value_rule)
end

---将所有IP白名单规则合并(暂时不合并,合并后会丢失所有msg信息)
-- _M.meger_ip_white = function(rules)
--     local s = "\n"
--     local tempS
--     local value_tbl = {}
--     for _, rule in ipairs(rules) do
--         if type(rule["value"]) == "table" then
--             tempS = "\"" .. table.concat(rule["value"], "\",\"") .. "\""
--             for _, ipstr in ipairs(rule["value"]) do
--                 table.insert(value_tbl, ipstr)
--             end
--         else
--             tempS = tostring(rule["value"])
--             table.insert(value_tbl, rule["value"])
--         end
--         s = s .. string.format("\n name:%s,var:%s,op:%s,value:%s", rule["name"], rule["var"], rule["op"], tempS)
--     end
-- end

return _M
